今天我們針對EdgeQL語法,分享一些進階的概念。
考慮schema如下:
type User {
    required name: str {
        constraint exclusive
    };
}
type Article {
    required title: str;
    author: User;
}
建立一個Article object與一個User object(簡稱為John):
insert Article {
    title:= "first article",
    author:= (insert User {name:= "John"})
};
此時如果想再建立一個Article object,其author link也是John的話,可以這麼寫:
insert Article {
    title:= "second article",
    author:= (select User filter .name="John")
};
由於User的name property是constraint exclusive,所以可以保證(select User filter .name="John")最多只會返回一個User object,確保author為single link。
但如果是執行下面這個query:
insert Article {
    title:= "second article",
    author:= (select User filter .name in {"John"})
};
EdgeDB則會報錯:
error: QueryError: possibly more than one element returned by an expression for a link 'author' declared as 'single'
這是因為in {}是一種set operation,需要加上assert_single()才可以順利執行:
insert Article {
    title:= "second article",
    author:= assert_single(
            (select User filter .name in {"John"})
    )
};
考慮schema如下:
abstract type Integer;
type PositiveInteger extending Integer;
type NegativeInteger extending Integer;
type Zero extending Integer;
insert兩個PositiveInteger object、一個NegativeInteger object及一個Zero object:
insert PositiveInteger;
insert PositiveInteger;
insert NegativeInteger;
insert Zero;
此時如果執行:
select Integer;
{
  default::PositiveInteger {id: 70699990-54c0-11ef-9775-df3dcebe0d15},
  default::PositiveInteger {id: 712a6d50-54c0-11ef-912e-ab352977af3c},
  default::Zero {id: 7db809b0-54c0-11ef-912e-8fa4e8f6afa6},
  default::NegativeInteger {id: 7b5512b2-54c0-11ef-912e-9fa4d590cb9d},
}
會選到剛剛insert的四個object。
此時如果我們仍然想由Integer object出發,來選取所有的PositiveInteger object的話,可以使用[is Type]的語法,像是`:
select Integer[Is PositiveInteger];
{
  default::PositiveInteger {id: 70699990-54c0-11ef-9775-df3dcebe0d15},
  default::PositiveInteger {id: 712a6d50-54c0-11ef-912e-ab352977af3c},
}
這樣就可以選取到兩個PositiveInteger object。
考慮一個模擬候選人與支持者關係的schema如下:
abstract type Person {
    required name: str { 
        constraint exclusive 
    };
}
type Candidate extending Person {
    party: str;
    multi supporters := .<endorsed_candidate[is Supporter];
}
type Supporter extending Person {
    endorsed_candidate: Candidate
}
insert兩個Candidate object及四個Supporter object。
insert Candidate {
    name:= "John",
    party:= "Party A",
};
insert Candidate {
    name:= "Cathy",
    party:= "Party B",
};
for name in {"Mark", "May"}
  union (
    insert Supporter {
        name := name,
        endorsed_candidate := (
            select Candidate filter .name = "John"
        )
     }
);
for name in {"Jeff", "Lisa"}
  union (
    insert Supporter {
        name := name,
        endorsed_candidate := (
            select Candidate filter .name = "Cathy"
        )
     }
);
假設我們想從Person出發,選擇所有的Person object,但卻只想展示Candidate object的property時,可以寫成:
select Person {name, [is Candidate].*};
{
  default::Candidate {
    name: 'John', 
    id: 433af8ae-54c7-11ef-8e3f-1f0e626fa8c9, 
    party: 'Party A'
  },
  default::Candidate {
    name: 'Cathy', 
    id: 4341299a-54c7-11ef-ade1-2bfcf9683bfe, 
    party: 'Party B'
  },
  default::Supporter {
    name: 'Mark', 
    id: 43591bfe-54c7-11ef-ade1-ef6840a01210, 
    party: {}
  },
  default::Supporter {
    name: 'May', 
    id: 4359270c-54c7-11ef-ade1-c3491717aa17, 
    party: {}
  },
  default::Supporter {
    name: 'Jeff', 
    id: 43aa506e-54c7-11ef-8e3f-dba6207b9cf4, 
    party: {}
  },
  default::Supporter {
    name: 'Lisa', 
    id: 43aa5352-54c7-11ef-8e3f-174d38b12195, 
    party: {}
  },
}
這樣的好處是選擇出來的shape是一致的。由於我們是針對Candidate object的property來選擇(只使用{*}),所以:
endorsed_candidate link沒有被選擇到。Supporter object的party property為空EdgeDBSet。如果想包含endorsed_candidate link的話,可以寫成:
select Person {name, [is Candidate].**};
{
  default::Candidate {
    name: 'John',
    id: 433af8ae-54c7-11ef-8e3f-1f0e626fa8c9,
    party: 'Party A',
    supporters: {
      default::Supporter {
        name: 'Mark', 
        id: 43591bfe-54c7-11ef-ade1-ef6840a01210
      },
      default::Supporter {
        name: 'May', 
        id: 4359270c-54c7-11ef-ade1-c3491717aa17
      },
    },
  },
  default::Candidate {
    name: 'Cathy',
    id: 4341299a-54c7-11ef-ade1-2bfcf9683bfe,
    party: 'Party B',
    supporters: {
      default::Supporter {
        name: 'Jeff', 
        id: 43aa506e-54c7-11ef-8e3f-dba6207b9cf4
      },
      default::Supporter {
        name: 'Lisa', 
        id: 43aa5352-54c7-11ef-8e3f-174d38b12195
      },
    },
  },
  default::Supporter {
    name: 'Mark', 
    id: 43591bfe-54c7-11ef-ade1-ef6840a01210, 
    party: {}, 
    supporters: {}
  },
  default::Supporter {
    name: 'May', 
    id: 4359270c-54c7-11ef-ade1-c3491717aa17, 
    party: {}, 
    supporters: {}
  },
  default::Supporter {
    name: 'Jeff', 
    id: 43aa506e-54c7-11ef-8e3f-dba6207b9cf4, 
    party: {}, 
    supporters: {}
  },
  default::Supporter {
    name: 'Lisa', 
    id: 43aa5352-54c7-11ef-8e3f-174d38b12195, 
    party: {}, 
    supporters: {}
  },
}
最後,如果您想從Candidate出發,卻只想選擇Person中的property的話,可以寫成:
select Candidate {Person.*};
{
  default::Candidate {
    id: 433af8ae-54c7-11ef-8e3f-1f0e626fa8c9, 
    name: 'John'
  },
  default::Candidate {
    id: 4341299a-54c7-11ef-ade1-2bfcf9683bfe, 
    name: 'Cathy'
  },
}
如果想包含Person中的property與link的話,可以寫成:
select Candidate {Person.**};
{
  default::Candidate {
    id: 433af8ae-54c7-11ef-8e3f-1f0e626fa8c9, 
    name: 'John'
  },
  default::Candidate {
    id: 4341299a-54c7-11ef-ade1-2bfcf9683bfe, 
    name: 'Cathy'},
}
不過因為我們的Person中沒有link,所以使用{*}及{**}的結果是一樣的。
本章延續前面splat的schema及database。
在研究type intersection的過程中,發現原來我們可以在對link使用shape時進行nested filter:
select Candidate {
    name,
    supporters: {name} filter .name="Mark"
} filter .name="John";
{
  default::Candidate {
    name: 'John', 
    supporters: {
      default::Supporter {name: 'Mark'}
    }
  }
}
query1先過濾了Candidate object的.name="John",接著再過濾了supporters link的.name="Mark"。
query2先過濾了Candidate object的.name="John",接著再過濾了Candidate object的.supporters.name="Mark":
select Candidate {
    name,
    supporters: {name}
} filter .name="John" and .supporters.name="Mark";
{
  default::Candidate {
    name: 'John',
    supporters: {
      default::Supporter {name: 'Mark'}, 
      default::Supporter {name: 'May'}
    },
  },
}
可以看出query2與query1的結果並不相同。
可能您會覺得奇怪為什麼query2中的supporters中會有兩個Supporter object呢?這是因為我們是針對Candidate的name property及supporters link來選擇,不管過濾的條件為何,Candidate object的shape已經決定。所以這相當於是在過濾了.name="John"後,再次確認.supporters.name="Mark"是否符合。如果都符合的話,則選取此Candidate object並展現其指定的shape。
可能您還是非常困惑,我相信這是因為對EdgeDB的element-wise特性還不夠熟悉所致。下面這個query應該能夠幫助您:
select Candidate.supporters.name = "Mark";
{true, false, false, false}
這個query的結果是將Candidate.supporters.name這個EdgeDBSet中的每個元素與「"Mark"」相比,如果相等的話返回true,否則返回false。
再回到query2,我們需要以and為分界來思考:
Candidate object,選出.name="John"為true的Candidate object,結果應該只有John一個。John(不是全部Candidate object,因為EdgeDB只需要針對and前半部返回true的Candidate object來篩選就好),選出符合.supporters.name="Mark"的Candidate object,結果應該還是只有John一個。這邊需留意此處其實進行了兩次比較,分別是「"Mark" = "Mark"」與「"May" = "Mark"」,由於只有「"Mark" = "Mark"」會返回true,所以返回John。假如name property不是constraint exclusive的話,而John的supporters有兩個Mark時,則此處會比較兩次「"Mark" = "Mark"」,並返回兩個John。考慮schema如下:
type Customer {
    name: str {
        constraint exclusive
    };
    cost: float64
}
Customer type有name property及cost property,分別記錄客人的名字及總消費金額。
當有客人進行消費時,我們想進行的query為:
Customer object,其cost property設為此次消費金額。Customer object,使其cost property為之前消費金額再加上此次消費金額。此時就可以運用到conflicts語法來處理。
首先我們假設John為name property為「"John"」的Customer object,而Cathy為name property為「"Cathy"」的Customer object。
執行下面query,代表John第一次消費「5」元:
insert Customer {
    name:= "John",
    cost:= 5
};
接著我們可以將John再次消費「10」元,與Cathy第一次消費「5」元的情形,合併寫為下面query:
with customers:= {(name:="John", cost:=10), (name:="Cathy", cost:=20)}
for customer in customers
union (
    insert Customer {
        name:= customer.name,
        cost:= customer.cost
    }
    unless conflict on .name
    else (
        with c:= (select Customer filter .name=customer.name)
        update c
        set {
            cost := .cost + customer.cost
        }
    )
);
上面query代表我們先試著insert一個User object,但是當其name property違反constraint exclusive時,代表該User object已經存在於資料庫。此時,我們可以執行else區塊內的update query。
此時,我們觀察所有User object:
select Customer{name, cost};
{
  default::Customer {name: 'Cathy', cost: 20}, 
  default::Customer {name: 'John', cost: 15}
}
可以確認:
John的兩次消費總額為5+10=15元,正確更新。Cathy的首次消費記錄為5元。本小節參考自官方文件。
考慮schema如下:
type Movie {
    title: str;
    release_year: int64
}
我們想選擇所有Movie object並根據下面兩點進行不同的排序:
title property作為排序對象。release_year property作為排序對象。這個query並不容易撰寫,原因是因為兩個property是不同型別的。為此官方文件給出建議方式是利用order by + then語法來完成。
我們先insert兩個Movie object:
insert Movie {title:= "Tom Wick", release_year:=2008};
insert Movie {title:= "Steel Man", release_year:=2014};
接著輸入官方建議的query:
select Movie {*}
  order by
    (.title if <str>$order_by = 'title'
      else <str>{})
  then
    (.release_year if <str>$order_by = 'release_year'
      else <int64>{});
此時當我們輸入:
title property為排序對象。release_year property為排序對象。<str>{}為排序對象。這是個有趣的技巧,但是實務上我也常常忘記有這種語法。
事實上,如果兩個property皆為同一型別的話,例如:
type Movie {
    title: str;
    release_year: str
}
我們可以使用多個if..else來簡化query,例如:
with order_by:= <str>$order_by
select Movie {*}
order by
(
    .title if order_by = 'title' else
    .release_year if order_by = 'release_year' else
    <str>{}
);
在同型別的情況下,我會傾向使用多個if..else語法。